@defikitdotnet/education-module-ai
Version:
AI Education Module using Agent Framework
833 lines (832 loc) • 73.7 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var jsx_runtime_1 = require("react/jsx-runtime");
var react_1 = require("react");
var router_1 = require("next/router");
var link_1 = __importDefault(require("next/link"));
var dynamic_1 = __importDefault(require("next/dynamic"));
var react_markdown_1 = __importDefault(require("react-markdown"));
var button_1 = require("../../../components/ui/button");
var card_1 = require("../../../components/ui/card");
var sonner_1 = require("sonner");
// Import components
var H5PContent = (0, dynamic_1.default)(function () { return Promise.resolve().then(function () { return __importStar(require("../../../components/H5PContent")); }); }, {
ssr: false,
});
var StudentContentView = function () {
console.log("--- StudentContentView Component Render Start ---");
var router = (0, router_1.useRouter)();
var courseId = router.query.id; // Rename id to courseId for clarity
var _a = (0, react_1.useState)(null), courseData = _a[0], setCourseData = _a[1];
var _b = (0, react_1.useState)(null), currentContentItem = _b[0], setCurrentContentItem = _b[1];
var _c = (0, react_1.useState)(true), loading = _c[0], setLoading = _c[1];
var _d = (0, react_1.useState)(null), error = _d[0], setError = _d[1];
var studentId = (0, react_1.useState)("student-1")[0]; // TODO: Replace with actual authenticated student ID
var _e = (0, react_1.useState)(new Map()), studentAnswers = _e[0], setStudentAnswers = _e[1]; // Map questionId -> answer
var _f = (0, react_1.useState)(false), submitting = _f[0], setSubmitting = _f[1];
var _g = (0, react_1.useState)(null), submissionResult = _g[0], setSubmissionResult = _g[1]; // Use the DTO type
var _h = (0, react_1.useState)(false), showAnswers = _h[0], setShowAnswers = _h[1]; // State for showing answers
var _j = (0, react_1.useState)(null), h5pScore = _j[0], setH5pScore = _j[1];
var _k = (0, react_1.useState)(null), enrollment = _k[0], setEnrollment = _k[1];
var _l = (0, react_1.useState)(new Set()), completedLectures = _l[0], setCompletedLectures = _l[1];
var _m = (0, react_1.useState)(false), updatingProgress = _m[0], setUpdatingProgress = _m[1];
var _o = (0, react_1.useState)(true), isAuthenticated = _o[0], setIsAuthenticated = _o[1]; // Default to true initially
var _p = (0, react_1.useState)(true), isAuthLoading = _p[0], setIsAuthLoading = _p[1]; // Add auth loading state
var _q = (0, react_1.useState)(null), student = _q[0], setStudent = _q[1]; // Add student state
var _r = (0, react_1.useState)(new Set()), completedQuizzes = _r[0], setCompletedQuizzes = _r[1]; // Track completed quizzes
var apiBaseUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:3009";
var teacherId = router.query.teacherId || (courseData === null || courseData === void 0 ? void 0 : courseData.teacherId) || ""; // Get teacherId for navigation
// --- DEBUG LOG: Log all state on every render ---
console.log("--- StudentContentView Render ---");
console.log("State:", {
courseId: courseId,
loading: loading,
error: error,
studentId: studentId,
currentContentItemId: currentContentItem === null || currentContentItem === void 0 ? void 0 : currentContentItem.id,
studentAnswersSize: studentAnswers.size,
isSubmitting: submitting,
hasSubmissionResult: !!submissionResult,
courseDataLoaded: !!courseData,
});
console.log("Current Answers Map:", studentAnswers);
console.log("--- End Render Log ---");
// --- END DEBUG LOG ---
// Fetch Course structure data
(0, react_1.useEffect)(function () {
var fetchCourseData = function () { return __awaiter(void 0, void 0, void 0, function () {
var response, errorData, data, err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!courseId || typeof courseId !== "string") {
setError("Invalid Course ID.");
setLoading(false);
return [2 /*return*/];
}
_a.label = 1;
case 1:
_a.trys.push([1, 6, 7, 8]);
setLoading(true);
setError(null);
return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/courses/").concat(courseId))];
case 2:
response = _a.sent();
if (!!response.ok) return [3 /*break*/, 4];
return [4 /*yield*/, response.json().catch(function () { return ({}); })];
case 3:
errorData = _a.sent();
throw new Error(errorData.error ||
"Failed to fetch course data (Status: ".concat(response.status, ")"));
case 4: return [4 /*yield*/, response.json()];
case 5:
data = _a.sent();
setCourseData(data);
// Set the initial content item (e.g., first lecture of first section)
if (data.sections &&
data.sections.length > 0 &&
data.sections[0].lectures &&
data.sections[0].lectures.length > 0) {
setCurrentContentItem(data.sections[0].lectures[0]);
}
else if (data.sections &&
data.sections.length > 0 &&
data.sections[0].quiz) {
// If first section has no lectures but has a quiz
setCurrentContentItem(data.sections[0].quiz);
}
return [3 /*break*/, 8];
case 6:
err_1 = _a.sent();
console.error("Error fetching course data:", err_1);
setError(err_1.message || "Failed to load course data. Please try again.");
return [3 /*break*/, 8];
case 7:
setLoading(false);
return [7 /*endfinally*/];
case 8: return [2 /*return*/];
}
});
}); };
fetchCourseData();
}, [courseId, apiBaseUrl]); // Depend on courseId
// Fetch enrollment and progress data for the student and course
(0, react_1.useEffect)(function () {
var fetchEnrollmentData = function () { return __awaiter(void 0, void 0, void 0, function () {
var progressResponse, _a, enrollment_1, completedLectureIds, completedQuizIds, lectureIds, quizIds, foundFirstUncompleted, mostRecentCompleted, _i, _b, section, _c, _d, lecture, errorData, err_2;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
// Wait for auth check to complete and confirm authentication
if (!courseId || typeof courseId !== "string" || isAuthLoading || !isAuthenticated || !courseData)
return [2 /*return*/];
_e.label = 1;
case 1:
_e.trys.push([1, 8, , 9]);
console.log("[Fetch Enrollment] Starting to fetch enrollment data...");
return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/enrollments/progress/").concat(courseId), { credentials: "include" })];
case 2:
progressResponse = _e.sent();
if (!progressResponse.ok) return [3 /*break*/, 4];
return [4 /*yield*/, progressResponse.json()];
case 3:
_a = _e.sent(), enrollment_1 = _a.enrollment, completedLectureIds = _a.completedLectures, completedQuizIds = _a.completedQuizzes;
console.log("[Fetch Enrollment] API Response OK:", {
enrollment: enrollment_1,
completedLectureIds: completedLectureIds,
completedQuizIds: completedQuizIds,
isArray: Array.isArray(completedLectureIds),
length: (completedLectureIds === null || completedLectureIds === void 0 ? void 0 : completedLectureIds.length) || 0
});
setEnrollment(enrollment_1);
lectureIds = new Set(Array.isArray(completedLectureIds) ? completedLectureIds : []);
console.log("[Fetch Enrollment] Setting completedLectures Set:", lectureIds);
setCompletedLectures(lectureIds);
quizIds = new Set(Array.isArray(completedQuizIds) ? completedQuizIds : []);
console.log("[Fetch Enrollment] Setting completedQuizzes Set:", quizIds);
setCompletedQuizzes(quizIds);
// Find the first uncompleted lecture and set it as the current content item
if (courseData && (lectureIds.size > 0 || quizIds.size > 0)) {
foundFirstUncompleted = false;
mostRecentCompleted = null;
// Look through all sections
for (_i = 0, _b = courseData.sections; _i < _b.length; _i++) {
section = _b[_i];
// First try to find the first uncompleted lecture
for (_c = 0, _d = section.lectures; _c < _d.length; _c++) {
lecture = _d[_c];
// If we've found an uncompleted lecture, set it as current and exit
if (!lectureIds.has(lecture.id)) {
console.log("[Navigation] Setting current item to first uncompleted lecture:", lecture.title);
setCurrentContentItem(lecture);
foundFirstUncompleted = true;
break;
}
else {
// Track the most recent completed lecture
mostRecentCompleted = lecture;
}
}
if (foundFirstUncompleted)
break;
// If all lectures in this section are complete, check if there's a quiz
if (section.quiz && !foundFirstUncompleted) {
// If quiz is not completed, navigate to it
if (!quizIds.has(section.quiz.id)) {
console.log("[Navigation] Setting current item to uncompleted quiz:", section.quiz.title);
setCurrentContentItem(section.quiz);
foundFirstUncompleted = true;
break;
}
}
}
// If all lectures are completed, set to the most recent one
if (!foundFirstUncompleted && mostRecentCompleted) {
console.log("[Navigation] All lectures completed, setting to most recent lecture:", mostRecentCompleted.title);
setCurrentContentItem(mostRecentCompleted);
}
}
return [3 /*break*/, 7];
case 4:
if (!(progressResponse.status === 404)) return [3 /*break*/, 5];
// Student is likely not enrolled (or auth issue server-side)
// Don't redirect immediately, show an error or a specific message
console.warn("Enrollment not found (404). User might not be enrolled or auth failed for API.");
setError("Enrollment not found. Please ensure you are enrolled in this course.");
setEnrollment(null); // Ensure enrollment state is null
return [3 /*break*/, 7];
case 5: return [4 /*yield*/, progressResponse.json().catch(function () { return ({}); })];
case 6:
errorData = _e.sent();
throw new Error(errorData.error || "Failed to fetch enrollment data (Status: ".concat(progressResponse.status, ")"));
case 7: return [3 /*break*/, 9];
case 8:
err_2 = _e.sent();
console.error("Error fetching enrollment data:", err_2);
setError(err_2.message || "Failed to load enrollment status.");
setEnrollment(null); // Ensure enrollment state is null on error
setCompletedLectures(new Set()); // Ensure it's empty on error
console.log("[Fetch Enrollment] Error setting completedLectures state Set to empty."); // <<< ADDED LOG
return [3 /*break*/, 9];
case 9: return [2 /*return*/];
}
});
}); };
fetchEnrollmentData();
// Add isAuthLoading and isAuthenticated as dependencies
}, [courseId, apiBaseUrl, isAuthLoading, isAuthenticated, courseData]);
// --- NEW: Function to explicitly mark the current lecture as complete ---
var markCurrentLectureComplete = (0, react_1.useCallback)(function () { return __awaiter(void 0, void 0, void 0, function () {
var lecture, response, _a, progress_1, isCompleted_1, errorData, err_3;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
// Only proceed if the current item is a lecture and not already completed
if (!currentContentItem || currentContentItem.hasOwnProperty('questions') || completedLectures.has(currentContentItem.id) || updatingProgress) {
console.log('[Mark Complete Trigger] Skipping:', { isLecture: currentContentItem && !currentContentItem.hasOwnProperty('questions'), isCompleted: currentContentItem && completedLectures.has(currentContentItem.id), updatingProgress: updatingProgress });
return [2 /*return*/];
}
lecture = currentContentItem;
console.log("[Mark Complete Trigger] Attempting to mark lecture ".concat(lecture.id, " as complete."));
setUpdatingProgress(true);
_b.label = 1;
case 1:
_b.trys.push([1, 7, 8, 9]);
return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/enrollments/progress/complete-lecture"), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
lectureId: lecture.id,
courseId: courseId,
}),
})];
case 2:
response = _b.sent();
if (!response.ok) return [3 /*break*/, 4];
return [4 /*yield*/, response.json()];
case 3:
_a = _b.sent(), progress_1 = _a.progress, isCompleted_1 = _a.isCompleted;
console.log('[Mark Complete] API Response OK:', { progress: progress_1, isCompleted: isCompleted_1 });
// Add to completed lectures set locally
setCompletedLectures(function (prev) {
var newSet = new Set(prev);
newSet.add(lecture.id);
console.log('[Mark Complete] Updated completedLectures Set:', newSet);
return newSet;
});
// Update enrollment state with new progress percentage from backend
setEnrollment(function (prev) {
if (!prev)
return prev;
var updatedEnrollment = __assign(__assign({}, prev), { progress: progress_1, completed: isCompleted_1 // Use completion status from backend response
});
console.log('[Mark Complete] Updated enrollment state:', updatedEnrollment);
return updatedEnrollment;
});
// If course is completed, show congratulations message
if (isCompleted_1) {
sonner_1.toast.success("Congratulations! You've completed this course!");
}
return [3 /*break*/, 6];
case 4: return [4 /*yield*/, response.json().catch(function () { return ({}); })];
case 5:
errorData = _b.sent();
console.error("[Mark Complete] API Error (".concat(response.status, "):"), errorData);
sonner_1.toast.error(errorData.message || "Failed to mark lecture complete (Status: ".concat(response.status, ")"));
_b.label = 6;
case 6: return [3 /*break*/, 9];
case 7:
err_3 = _b.sent();
console.error("[Mark Complete] Fetch Error:", err_3);
sonner_1.toast.error("An error occurred while saving progress.");
return [3 /*break*/, 9];
case 8:
setUpdatingProgress(false);
return [7 /*endfinally*/];
case 9: return [2 /*return*/];
}
});
}); }, [currentContentItem, courseId, apiBaseUrl, completedLectures, updatingProgress, setCompletedLectures, setEnrollment, sonner_1.toast]); // Dependencies for the callback
// Initialize or reset answers when a quiz is selected
var initializeAnswers = (0, react_1.useCallback)(function (quiz) {
console.log("Initializing/Resetting answers for quiz:", quiz.id); // DEBUG LOG
var initialAnswers = new Map();
quiz.questions.forEach(function (q) {
initialAnswers.set(q.id, { questionId: q.id, answer: "" });
});
setStudentAnswers(initialAnswers);
setSubmissionResult(null); // Clear previous results
setError(null); // Clear previous submission errors
}, []); // Added useCallback with empty dependency array
// Update state when a new content item is selected from sidebar
var handleContentSelect = function (item) {
console.log("handleContentSelect: Setting current item to:", item.id, item.title); // Keep this log
setCurrentContentItem(item);
};
// --- NEW useEffect to initialize answers when currentContentItem is a quiz ---
(0, react_1.useEffect)(function () {
if (currentContentItem && "questions" in currentContentItem) {
// Check if it's a Quiz
console.log("currentContentItem changed to a Quiz, initializing answers:", currentContentItem.id); // DEBUG LOG
initializeAnswers(currentContentItem);
}
else if (currentContentItem) {
// It's a lecture or other non-quiz item
console.log("currentContentItem changed to a Lecture/Other, clearing answers."); // DEBUG LOG
setStudentAnswers(new Map()); // Clear answers if it's not a quiz
setSubmissionResult(null); // Clear results
setError(null); // Clear errors
}
// This effect should run ONLY when currentContentItem changes
}, [currentContentItem, initializeAnswers]);
// --- Answer Handling (Modified for Quiz) ---
var handleQuizAnswerChange = function (questionId, answer) {
console.log("handleQuizAnswerChange called for question ".concat(questionId, " with answer:"), answer); // DEBUG LOG
setStudentAnswers(function (prev) {
var newState = new Map(prev).set(questionId, { questionId: questionId, answer: answer });
console.log("New studentAnswers state:", newState); // DEBUG LOG
return newState;
});
};
// Handle submitting QUIZ answers
var handleSubmitQuiz = function () { return __awaiter(void 0, void 0, void 0, function () {
var quizId, answersPayload, unansweredQuestions, response, responseData, quizId_1, quizCompletionResponse, _a, progress_2, isCompleted_2, err_4, err_5, errorMessage;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
// Ensure current item is a quiz and has questions
if (!currentContentItem ||
!("questions" in currentContentItem) ||
currentContentItem.questions.length === 0) {
sonner_1.toast.error("Cannot submit, no quiz selected or quiz has no questions.");
return [2 /*return*/];
}
quizId = currentContentItem.id;
answersPayload = Array.from(studentAnswers.values());
unansweredQuestions = answersPayload.filter(function (ans) {
return ans.answer === "" || ans.answer === undefined || ans.answer === null;
});
if (unansweredQuestions.length > 0) {
sonner_1.toast.warning("Please answer all ".concat(currentContentItem.questions.length, " questions before submitting. (").concat(unansweredQuestions.length, " remaining)"));
return [2 /*return*/];
}
_b.label = 1;
case 1:
_b.trys.push([1, 12, 13, 14]);
setSubmitting(true);
setError(null);
setSubmissionResult(null);
setShowAnswers(false); // Reset show answers on new submission
return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/student/quizzes/").concat(quizId, "/submit"), {
method: "POST",
headers: {
"Content-Type": "application/json",
// Add authentication headers if required by your backend (e.g., JWT token)
// 'Authorization': `Bearer ${yourAuthToken}`,
},
credentials: "include", // Ensure cookies are sent
body: JSON.stringify({
// Use the actual student ID from auth state
studentId: student === null || student === void 0 ? void 0 : student.id, // Use the student ID from state
answers: answersPayload,
}),
})];
case 2:
response = _b.sent();
return [4 /*yield*/, response.json()];
case 3:
responseData = _b.sent();
if (!response.ok) {
// Throw an error with the message from the backend if available
throw new Error(responseData.error ||
responseData.message ||
"HTTP error! status: ".concat(response.status));
}
if (!responseData.data) return [3 /*break*/, 10];
setSubmissionResult(responseData.data);
sonner_1.toast.success(responseData.message || "Quiz submitted successfully!");
if (!(currentContentItem && "questions" in currentContentItem)) return [3 /*break*/, 9];
quizId_1 = currentContentItem.id;
setCompletedQuizzes(function (prev) {
var newSet = new Set(prev);
newSet.add(quizId_1);
console.log("[Quiz Complete] Added quiz to completedQuizzes:", quizId_1);
return newSet;
});
_b.label = 4;
case 4:
_b.trys.push([4, 8, , 9]);
return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/enrollments/quiz-complete"), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
quizId: quizId_1,
courseId: courseId,
score: responseData.data.score
}),
})];
case 5:
quizCompletionResponse = _b.sent();
if (!quizCompletionResponse.ok) return [3 /*break*/, 7];
return [4 /*yield*/, quizCompletionResponse.json()];
case 6:
_a = _b.sent(), progress_2 = _a.progress, isCompleted_2 = _a.isCompleted;
console.log("[Quiz Complete] Progress updated:", { progress: progress_2, isCompleted: isCompleted_2 });
// Update enrollment with new progress
setEnrollment(function (prev) {
if (!prev)
return prev;
return __assign(__assign({}, prev), { progress: progress_2, completed: isCompleted_2 // Use completion status from backend response
});
});
_b.label = 7;
case 7: return [3 /*break*/, 9];
case 8:
err_4 = _b.sent();
console.error("Error recording quiz completion:", err_4);
return [3 /*break*/, 9];
case 9: return [3 /*break*/, 11];
case 10:
// Handle unexpected success response format
console.warn("Quiz submission successful, but response data format unexpected:", responseData);
sonner_1.toast.success("Quiz submitted, but couldn't display results.");
_b.label = 11;
case 11: return [3 /*break*/, 14];
case 12:
err_5 = _b.sent();
console.error("Error submitting quiz answers:", err_5);
errorMessage = err_5.message || "Failed to submit quiz answers. Please try again.";
setError(errorMessage);
sonner_1.toast.error(errorMessage);
return [3 /*break*/, 14];
case 13:
setSubmitting(false);
return [7 /*endfinally*/];
case 14: return [2 /*return*/];
}
});
}); };
// Handle H5P completion (Assuming H5P is linked to a lecture)
var handleH5PCompleted = function (score, maxScore) {
if (!currentContentItem || "questions" in currentContentItem)
return; // Only for lectures
var lectureId = currentContentItem.id;
setH5pScore({ score: score, maxScore: maxScore });
sonner_1.toast.info("H5P score recorded: ".concat(score, "/").concat(maxScore));
// TODO: Need a backend endpoint to record H5P completion for a specific lecture attempt
// Example: /api/student/lectures/:lectureId/h5p-complete
console.log("Recording H5P completion:", {
lectureId: lectureId,
studentId: student === null || student === void 0 ? void 0 : student.id,
score: score,
maxScore: maxScore,
});
fetch("".concat(apiBaseUrl, "/api/student/lectures/").concat(lectureId, "/h5p-complete"), {
// Hypothetical endpoint
method: "POST",
headers: { "Content-Type": "application/json", credentials: "include" }, // Add credentials
body: JSON.stringify({ studentId: student === null || student === void 0 ? void 0 : student.id, score: score, maxScore: maxScore }), // Use student ID from state
}).catch(function (err) {
console.error("Error recording H5P completion:", err);
sonner_1.toast.error("Failed to save H5P progress.");
});
};
// Format date for display
var formatDate = function (dateString) {
if (!dateString)
return "N/A";
try {
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
}
catch (e) {
return "Invalid Date";
}
};
// Helper to parse JSON safely
var safeJsonParse = function (jsonString, defaultValue) {
if (defaultValue === void 0) { defaultValue = null; }
if (!jsonString || typeof jsonString !== "string") {
console.warn("safeJsonParse: Input is not a valid string", jsonString);
return defaultValue;
}
try {
return JSON.parse(jsonString.trim());
}
catch (e) {
console.error("safeJsonParse: Failed to parse JSON string:", jsonString, e);
return defaultValue; // Return default if parsing fails
}
};
// --- Action Handlers for Post-Quiz Options ---
var handleRetakeQuiz = function () {
if (!currentContentItem || !("questions" in currentContentItem))
return;
console.log("Retaking quiz:", currentContentItem.id);
setSubmissionResult(null); // Clear previous result
setShowAnswers(false); // Hide answers view
setError(null); // Clear previous errors
initializeAnswers(currentContentItem); // Re-initialize/clear answers map
sonner_1.toast.info("Quiz reset. Please answer the questions again.");
};
var handleShowAnswersClick = function () {
console.log("Showing answers...");
setShowAnswers(true);
// Check if backend provided answer details
if (!(submissionResult === null || submissionResult === void 0 ? void 0 : submissionResult.answerDetails) ||
submissionResult.answerDetails.length === 0) {
// This might happen if backend failed to generate details
sonner_1.toast.warning("Detailed answer explanations could not be loaded.");
}
else {
sonner_1.toast.info("Showing detailed answers.");
}
};
var findNextContentItem = function () {
var _a;
if (!courseData || !currentContentItem)
return null;
var currentSectionIndex = -1;
var currentItemIndex = -1;
var isQuiz = "questions" in currentContentItem;
// Find the current section and item index
for (var i = 0; i < courseData.sections.length; i++) {
var section = courseData.sections[i];
if (isQuiz && ((_a = section.quiz) === null || _a === void 0 ? void 0 : _a.id) === currentContentItem.id) {
currentSectionIndex = i;
currentItemIndex = section.lectures.length; // Quiz is considered after all lectures in a section
break;
}
else if (!isQuiz) {
var lectureIndex = section.lectures.findIndex(function (l) { return l.id === currentContentItem.id; });
if (lectureIndex !== -1) {
currentSectionIndex = i;
currentItemIndex = lectureIndex;
break;
}
}
}
if (currentSectionIndex === -1)
return null; // Should not happen
var currentSection = courseData.sections[currentSectionIndex];
// Check if there's a next lecture in the current section
if (currentItemIndex < currentSection.lectures.length - 1) {
return currentSection.lectures[currentItemIndex + 1];
}
// Check if there's a quiz after the last lecture in the current section
if (!isQuiz &&
currentItemIndex === currentSection.lectures.length - 1 &&
currentSection.quiz) {
return currentSection.quiz;
}
// Check if there's a next section
if (currentSectionIndex < courseData.sections.length - 1) {
var nextSection = courseData.sections[currentSectionIndex + 1];
// Return first lecture of next section, or its quiz if no lectures
if (nextSection.lectures.length > 0) {
return nextSection.lectures[0];
}
else if (nextSection.quiz) {
return nextSection.quiz;
}
}
// No more content items
return null;
};
var handleNextSection = function () { return __awaiter(void 0, void 0, void 0, function () {
var nextItem;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: // Make async
// *** Mark current lecture complete BEFORE navigating ***
return [4 /*yield*/, markCurrentLectureComplete()];
case 1:
// *** Mark current lecture complete BEFORE navigating ***
_a.sent();
nextItem = findNextContentItem();
if (nextItem) {
console.log("Navigating to next item:", nextItem.id, nextItem.title);
handleContentSelect(nextItem);
// Reset quiz-specific states if navigating away from a quiz result
setShowAnswers(false);
// submissionResult is kept to show completion status maybe? Or clear it?
// setSubmissionResult(null); // Optional: clear result when moving on
}
else {
// This case should ideally be handled by the Finish Course button now
console.log("No next section found. (handleNextSection)");
sonner_1.toast.success("Congratulations! You have completed the course content.");
// Navigate back to dashboard if button wasn't clicked for some reason
router.push("/student?teacherId=".concat(teacherId));
}
return [2 /*return*/];
}
});
}); };
var handleFinishCourse = function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: // Make async
// *** Mark current lecture complete BEFORE finishing ***
return [4 /*yield*/, markCurrentLectureComplete()];
case 1:
// *** Mark current lecture complete BEFORE finishing ***
_a.sent();
console.log("Finishing course and returning to dashboard...");
sonner_1.toast.success("Congratulations! Course completed.");
// Reset state related to the current quiz before navigating
setSubmissionResult(null);
setShowAnswers(false);
setStudentAnswers(new Map());
// Navigate back to the student dashboard, ensuring teacherId is passed if needed
router.push("/student?teacherId=".concat(teacherId));
return [2 /*return*/];
}
});
}); };
// Check authentication status and get student data
(0, react_1.useEffect)(function () {
var checkAuthStatus = function () { return __awaiter(void 0, void 0, void 0, function () {
var response, data, errorText, err_6;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
setIsAuthLoading(true); // Start loading
console.log("[Auth Check] Starting..."); // Log start
_a.label = 1;
case 1:
_a.trys.push([1, 7, 8, 9]);
return [4 /*yield*/, fetch("".concat(process.env.NEXT_PUBLIC_BACKEND_URL, "/api/auth/student/status"), {
credentials: "include"
})];
case 2:
response = _a.sent();
console.log("[Auth Check] Raw response status: ".concat(response.status)); // Log status
if (!response.ok) return [3 /*break*/, 4];
return [4 /*yield*/, response.json()];
case 3:
data = _a.sent();
console.log("[Auth Check] Response data:", data); // Log parsed data
setIsAuthenticated(data.isAuthenticated);
setStudent(data.student || null); // Store student data
if (!data.isAuthenticated) {
sonner_1.toast.error("Authentication required. Redirecting...");
console.log("[Auth Check] Not authenticated, redirecting to dashboard."); // Log redirect reason
// Keep loading true while redirecting
router.push("/student?teacherId=".concat(teacherId));
// No need to setLoading(false) here as the component will unmount
return [2 /*return*/]; // Exit early
}
console.log("[Auth Check] Authenticated. Proceeding."); // Log success
return [3 /*break*/, 6];
case 4: return [4 /*yield*/, response.text()];
case 5:
errorText = _a.sent();
console.error("[Auth Check] Non-OK response: ".concat(response.status), errorText); // Log non-OK response
setIsAuthenticated(false);
setStudent(null);
sonner_1.toast.error("Authentication check failed (".concat(response.status, "). Redirecting..."));
console.log("[Auth Check] Non-OK response, redirecting to dashboard."); // Log redirect reason
router.push("/student?teacherId=".concat(teacherId));
return [2 /*return*/]; // Exit early
case 6: return [3 /*break*/, 9];
case 7:
err_6 = _a.sent();
console.error("[Auth Check] Fetch error:", err_6); // Log fetch error
setIsAuthenticated(false);
setStudent(null);
sonner_1.toast.error("Error checking authentication. Redirecting...");
console.log("[Auth Check] Fetch error, redirecting to dashboard."); // Log redirect reason
router.push("/student?teacherId=".concat(teacherId));
return [2 /*return*/]; // Exit early
case 8:
// Only set loading false if we are NOT redirecting
if (router.asPath.includes('/student/content/')) { // Check if we are still on the page
console.log("[Auth Check] Finished, setting AuthLoading to false.");
setIsAuthLoading(false);
}
else {
console.log("[Auth Check] Finished, but already redirected.");
}
return [7 /*endfinally*/];
case 9: return [2 /*return*/];
}
});
}); };
checkAuthStatus();
}, [apiBaseUrl, router, teacherId]); // Dependencies
// Render the appropriate content based on the current item's type
var renderContentItem = function () {
var _a;
if (!currentContentItem) {
return ((0, jsx_runtime_1.jsx)("p", { className: "text-gray-500", children: "Select an item from the sidebar to view content." }));
}
var nextItem = findNextContentItem(); // Check if there is a next item
var isLastItem = !nextItem; // Determine if this is the last item
// --- LECTURE ---
if (!("questions" in currentContentItem)) {
// It's a Lecture
var lecture_1 = currentContentItem;
var contentType = lecture_1.type;
// Parse content ONLY if it's expected to be JSON (video/h5p)
var contentData = (contentType === 'video' || contentType === 'h5p')
? safeJsonParse(lecture_1.content)
: null; // For text type, contentData is not needed, we use lecture.content directly
return ((0, jsx_runtime_1.jsxs)("div", { children: [" ", (0, jsx_runtime_1.jsxs)(card_1.Card, { className: "mb-6 dark:bg-gray-800", children: [(0, jsx_runtime_1.jsx)(card_1.CardHeader, { children: (0, jsx_runtime_1.jsx)(card_1.CardTitle, { children: lecture_1.title }) }), (0, jsx_runtime_1.jsxs)(card_1.CardContent, { children: [contentType === "text" && ((0, jsx_runtime_1.jsx)("div", { className: "space-y-4 text-gray-700 dark:text-gray-300", children: (0, jsx_runtime_1.jsx)(react_markdown_1.default, { components: {
code: function (_a) {
var node = _a.node, inline = _a.inline, className = _a.className, children = _a.children, props = __rest(_a, ["node", "inline", "className", "children"]);
var match = /language-(\w+)/.exec(className || "");
// Style differently for inline code vs code blocks
return !inline ? ((0, jsx_runtime_1.jsx)("pre", { className: "bg-gray-100 dark:bg-gray-900 p-4 rounded-md overflow-x-auto my-4", children: (0, jsx_runtime_1.jsx)("code", __assign({ className: "font-mono text-sm ".concat(className) }, props, { children: typeof lecture_1.content === "string"
? lecture_1.content.replace(/\\n/g, "\n")
: "Lecture content is not available." })) })) : (
// Inline code style (optional, can adjust)
(0, jsx_runtime_1.jsx)("code", __assign({ className: "font-mono text-sm bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded ".concat(className) }, props, { children: children })));
},
}, children: typeof lecture_1.content === "string"
? lecture_1.content.replace(/\\n/g, "\n")
: "Lecture content is not available." }) })), contentType === "video" && (contentData === null || contentData === void 0 ? void 0 : contentData.url) && ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("p", { className: "mb-2 text-sm text-gray-600", children: ["Video URL:", " ", (0, jsx_runtime_1.jsx)("a", { href: contentData.url, target: "_blank", rel: "noopener noreferrer", className: "text-indigo-600 hover:underline", children: contentData.url })] }), contentData.url.includes("youtube.com") ||
contentData.url.includes("youtu.be") ? ((0, jsx_runtime_1.jsx)("iframe", { width: "100%", height: "480" // Adjust height as needed